基于异常上线场景的实时拦截与问题分发策略
点击蓝字,关注我们
GEEK TALK
01
背景
1.1 业务背景
1.2 技术背景
实时UV计算:在处理异常上线的拦截过程中,数据的实时消费以及数据的时效性的要求特别高,必须在分钟级别内完成。同时,业务方不仅需要获得异常的PV数,同时也需要获得在各个维度下异常影响的用户数(UV)。但实时UV计算不能简单地累加,这涉及到同维度间用户交集的处理。例如:A版本和B版本异常影响的用户数分别是100,但整体用户数实际上可能不足200。但是也不能直接存储用户ID,例如通过使用HashSet或者HashMap存储所有的用户ID进行去重,这面对大量用户时,会占用大量的计算节点内存资源。因此,我们采取一种计算的时效性和准确性较高的数据结构Bitmap来计算实时UV。
Bitmap 的底层数据结构用的是 String 类型的 SDS 数据结构来保存位数组,把每个字节数组的 8 个 bit 位利用起来,每个 bit 位 表示一个元素的二值状态。该数据结构能节约大量的存储,同时在用户群做交集和并集运算的时候也有极大的便利。例如,每个用户ID如果存储在HashSet或者HashMap中,需要占4个字节即32bit,而一个用户在Bitmap中只占一个bit。在做交并集运算时,例如,员工1、员工2都是程序员,员工1使用苹果手机,那么如何查找使用苹果手机的程序员用户?
直接使用位运算,使用苹果手机的程序员用户:(0000000110B & 0000000010B = 0000000010B)
异常反混淆:主要用于问题分发阶段。APP厂商在发布应用程序包时,通常会对包进行混淆操作,这是为了提高APP应用的安全性和减少反编译的风险。混淆是将源代码中的符号、名称和结构等转换为难以理解的形式,使得反编译后的代码难以还原为原始的源代码,但是APP上报的异常信息也被混淆了。反混淆操作是将混淆后的异常信息还原为可读的形式,使开发人员能够更准确地分析问题的原因,并迅速采取正确的修复措施。在APP产出应用程序包时,同时也会产生一份用于反混淆异常信息的映射文件(密码本),通过映射文件 + 解析算法对混淆的异常进行解析,即可得到已读的异常堆栈。
△异常信息反混淆过程
1.3 名词解释
性能中台:性能中台是APP性能追踪的一站式解决方案平台,为APP提供全面、实时的性能分析服务与工具链。
移动线上质量平台: 移动线上质量平台是移动端APP发包后,用来查看、分析包质量数据、进行核心指标监控/报警、变更异常拦截。
日志中台:指端日志中台,包括端日志全生命周期的能力建设。包括打点SDK / 打点server/ 日志管理平台等核心组件。
Tekes平台:App端研发平台,提供包组件管理等基础设施。
02
2.1 整体流程
在以往的流程中,针对客户端上线变更,我们通常使用大盘性能指标来进行监控,以便进行问题定位和止损。然而,在灰度用户数量较少的情况下,线上问题往往无法在大盘性能指标中产生明显波动。当业务决定全量上线或扩大灰度用户规模时,问题就可能显现出来了。在问题定位和解决的阶段,我们过多地依赖人工干预和手动排查,这导致问题定位和解决的时间较长,并可能升级为事故。因此,在旧流程中,线上问题影响面大小主要取决于灰度用户的规模以及问题排查人员对客户端各个模块和相关人员的了解程度,这是不合理的。因此,系统设计的关键在于解决两个核心问题:首先,如何在灰度阶段拦截问题,避免其进一步扩大;其次,一旦线上问题出现,如何能够迅速进行问题召回与解决。
△线上异常实时拦截与问题分发整体流程设计图
因此,在新流程中,引入了两个关键模块:"变更拦截模块"和"问题分发模块"。对于每次平台的上线变更,必须先在变更拦截模块中进行注册,从而生成一个唯一的上线染色ID。同时,将染色ID下发至本次变更上线的用户客户端。此后,该数据集的用户日志将携带染色ID进行上报。变更拦截模块将基于染色ID的粒度进行监控和拦截,以保证在上线过程中问题的及时发现。同时,问题分发模块建立了问题自动分发机制。当一个上线变更被拦截或者在线上出现问题时,该模块将直接将问题指派给涉及问题的模块、组件,以及相关的研发和测试人员。协助业务方快速准确的定位问题,人工再介入修复。
2.2 异常上线变更拦截
异常上线变更拦截的核心思路是:为每次上线变更生成独特的染色ID,通过对每个染色ID的性能核心数据进行拦截与监控。
△异常上线变更拦截流程设计图
变更拦截流程的具体步骤如下:
① 变更上线注册: 针对厂内各个配置变更平台,需要在每次上线配置生效之前,将上线信息在染色通用服务进行注册。
② 获取染色ID: 染色通用服务通过HTTP接口为每次上线注册生成通用的染色ID,并将其返回给上线变更平台。
③ 下发染色配置: 在圈定的用户群范围内,上线变更平台将新配置和染色信息同时下发到端上的业务SDK(如AB-SDK)中。
④ 染色日志上报: 业务SDK会判定染色是否生效,如果生效,则在涉及性能核心场景的日志中附加染色ID信息,然后通过UBC-SDK上报。这些日志会通过日志中台实时转发,并写入消息队列。
⑤ 实时指标计算: 性能中台会实时订阅消息队列中的核心性能数据,例如崩溃、APP启动次数等,然后针对每个染色ID,根据多个维度(如产品线、APP版本、操作系统、地域等)形成性能聚合指标,并将其写入持久存储。
⑥ 异常拦截服务: 基于存储中的染色数据,异常拦截服务通过配置监控项来检测数据是否出现异常。一旦染色数据异常,系统会触发拦截措施并发出告警。
⑦ 异常止损: 在触发拦截和告警后,系统会通过关联染色ID和变更上线的关系,拦截继续放量以及通知业务方针对本次上线的配置回滚。
针对每次线上配置的变更,都会有一段观察期。这个观察期的长短需要适度,以确保数据的可靠性。过短的观察期会影响数据的置信度,而过长则可能降低研发效率。一般而言,每次变更后需要等待10分钟的观察期,然后再逐步增加线上流量。因此,变更拦截模块对数据时效性的要求非常高,要求数据的端到端传输时效在3分钟以内,以确保有足够的数据累积时间,从而提升监控指标的可信度。同时,染色ID的数据指标项不仅涵盖了基础的PV指标(如崩溃次数和APP启动用户数),还扩展至UV级别的指标(如受崩溃影响的用户数收敛情况和APP用户启动数)。多个指标维度下UV的计算也给数据链路的时效性和准确性带来了挑战。具体的数据流设计如下所示:
△变图更拦截数据流设计
变更拦截的数据流主要分为两个部分:
(1)实时指标计算服务;
(2) ID生成服务。
ID生成服务
ID生成服务主要用于将厂内的CUID生成INT类型的数字,从而存储到Bitmap的数据结构中。整个服务需要满足以下条件:
(1)CUID-ID的的映射全局唯一,不会出现重复的ID,且ID的整体趋势递增。
(2)高并发低延时。核心数据在计算节点内存中进行产出,减少数据库压力。
(3)高可用,服务基于云上分布式架构,即使存储mysql宕机,也能容忍一段时间数据库不可用。
在实时流中,在接收到原始数据时,先根据CUID进行keyby分发,将相同的CUID分发到同一个计算节点。对于每个计算节点:
(1)优先查询自身内存中的缓存的映射关系,若不存在,则查询redis。
(2)若redis不存在映射关系,则访问生成新ID服务。
(3)服务请求hash到号段节点上,每次去DB拿固定长度的ID List进行分发,然后把最大的ID持久化下来,也就是并非每个ID都做持久化,仅仅持久化一批ID中最大的那一个。这个方式有点像游戏里的定期存档功能,只不过存档的是未来某个时间下发给用户的ID,这样极大地减轻了DB持久化的压力。
(4)最终将映射关系写入到Redis存储中。
实时指标计算服务
在数据处理流程中,端上传的日志数据经由日志中台进行转发,进而分发到性能的各个消息队列中。实时计算服务订阅这些消息队列中的数据,以多级聚合方式进行维度指标的计算:
(1)数据分发与映射: 首先,从消息队列中获取原始数据。根据CUID进行keyby分发,将相同的CUID分发到同一个计算节点。防止相同的CUID同时访问ID-Mapping服务,导致CUID-ID的的映射全局不唯一。
(2)本地聚合: 对数据进行解析获得指标和维度,然后在每个计算节点上进行本地窗口聚合操作,将具有相同维度(版本、操作系统、染色ID等)的CUID汇总成Bitmap格式。本地聚合的目的在于减少后续的keyby shuffle阶段的数据量。
(3)全局聚合: 状态服务维护实时流的运行时状态信息和历史数据的Bitmap结果。将实时数据与历史数据进行全局聚合,从而得到最终的结果数据。同时,新的Bitmap结重新写入状态服务。其中,运行时状态信息保证了在实时流断流或重启时,能够恢复上次运行状态。加上可重入的数据源和幂等的数据输出,确保了数据流的不丢不重。
通过上述服务,当新的变更上线导致端上异常数据突增时,变更拦截服务能够在分钟级内对异常上线进行监控告警以及拦截。此外,除了向业务方披露拦截的数据指标,我们也希望中台能够直接协助业务方定位排查出问题的根因。
2.3 问题自动分发
△线上问题实时自动分发数据流
(1)映射文件写入存储
(2)线上异常反混淆
(3)端组件关系建立
(4)问题分发
其中(1)(2)步骤是为了将线上异常解析为可读的形式。(3)(4)步骤为将可读的堆栈进行聚类以及分发。
映射文件写入存储
线上异常反混淆
△状态服务数据流设计
(1)同时间窗口数据关联:从消息队列中订阅数据后,首先会对处于计算节点同一个时间窗口的数据进行关联,若关联成功,则数据直接发往下游进行计算。
(2)同历史未关联的数据关联:若未成功,则查询状态服务中之前窗口中尚未被关联的数据进行关联,若关联成功,发往下游,状态存储中清除关联数据。
(3)未关联数据写入状态存储:若未成功,则将未关联的数据写入到状态存储中,等待被将来的数据进行关联。
△多级索引查询
但是,随着线上任务长时间运行时,我们注意到程序性能逐渐下降,导致实时流任务的数据处理经常出现延迟。我们发现问题的根本原因是计算节点缓存中的映射文件被频繁替换,导致缓存命中率低。因此,我们采用了更为适合业务场景的缓存替代算法---W-TinyLFU代替常规的LRU缓存替代策略。
△W-TinyLFU算法
相比LRU算法,W-TinyLFU:
1、热点数据适应性更强: 在高流量的场景中,一些热点数据项可能会在短时间内被多次访问。与LRU只关注最近的访问,W-TinyLFU通过维护频繁访问计数来更好地捕获这种热点数据的特征,从而更好地适应瞬时的流量变化。
2、低频数据保护:LRU在遇到新数据时,会立即淘汰最近最少使用的数据,这可能导致低频数据被频繁淘汰。W-TinyLFU通过维护一个近似的频率计数,可以更好地保护低频数据,防止它们被过早地淘汰。
3、适应性更强:W-TinyLFU在面对访问模式的变化时,能够更快地适应新的访问模式。它在长时间内持续观察访问模式,并逐渐调整数据项的权重,以更好地反映最近的访问模式。
4、写入操作考虑:LRU通常对写入操作的适应性较差,因为写入操作可能导致数据被立即淘汰。W-TinyLFU考虑了写入操作,通过维护写入时的频繁访问计数,可以更好地处理写入操作。
5、内存效率:W-TinyLFU使用了一些压缩技术来存储频繁访问计数,从而在一定程度上减少了内存占用。
经过对线上异常进行反混淆处理后,我们获得了可读的堆栈信息。接下来,我们可以对这些可读的堆栈信息进行问题聚类和分发。
端组件关系建立
组件关系变更的数据来源分为两个部分:
(1)全量数据:在APP进行发版时,通过EasyBox等工具将组件、模块等关系从包中解析出来,将该版本的组件信息上传到组件管理平台,触发组件管理平台的全量同步,将组件信息写入消息队列中。中台通过订阅组件数据,建立类方法<->组件、模块、研发人员、测试人员的关系,写入到存储中。
(2)增量数据:在组件管理平台进行人为的修改,例如修改组件类的研发、测试负责人等,触发增量同步,变更的数据写入消息队列。
通过上述数据同步,建立了类方法<->人的映射。
问题分发
通过数据流的反混淆解析,我们成功地将异常信息从二进制地址转换为可读的信息。接下来,借助聚类规则算法,我们将不同的异常堆栈逐行遍历,并优先将其聚类到厂内维护的组件和模块中所包含的类中。在此过程中,我们建立了线上问题<->类之间的关系。而在端组件关系建立的流程中,我们成功地构建了类方法<->研发测试人员之间的映射关系。将这两者的关系结合起来,我们获得了问题<->人员之间的关联。因此,当线上出现问题时,无需人工干预,系统可以直接将该问题指向负责该问题模块的负责人。负责人随后可以根据反混淆后的异常信息,进行问题的排查和修复工作。
03
本文主要介绍了性能中台在处理异常上线过程中,针对变更拦截和问题分发方面做的一些努力。当然,后面我们还会继续更好的服务业务,如:
1、提升影响力:对接更多的上线变更平台以及落地更多的APP。
2、提升场景覆盖度:覆盖卡顿、启动速度、网络性能、搜索性能等更多的核心业务场景。
3、提升问题聚类以及分发策略的准确度:将线上问题分发的更加合理与准确。
希望,性能中台持续不断优化,为保障APP的质量做出贡献。
END
推荐阅读